﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using System.Web.Compilation;
using System.Web;
using System.Web.Routing;
using Newtonsoft.Json;

namespace MVCPlugin.Utility
{
    public static class Plugin
    {
        internal static string METADATA_FILE_NAME = "Metadata.config";
        private static string STATUS_FILE_NAME = "status.txt";
        internal const string BASE_FOLDER_NAME = "Plugins";
        internal static readonly string PlugBaseFolderFullPath = Path.Combine(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, BASE_FOLDER_NAME);
        private static readonly string SHADOW_COPY_FOLDER = Path.Combine(PlugBaseFolderFullPath, "bin");
        private static ILogger Logger = new ConsoleLogger();

        public static List<PluginMetadata> GetAllValidPluginMetadatas()
        {
            return CacheHelper.GetWithHttpContextCache<List<PluginMetadata>>("Plugin.GetAllValidPluginMetadatas", () =>
            {
                DirectoryInfo dir = new DirectoryInfo(PlugBaseFolderFullPath);
                if (dir.Exists == false)
                {
                    return new List<PluginMetadata>(0);
                }
                DirectoryInfo[] subDirArray = dir.GetDirectories("*", SearchOption.TopDirectoryOnly);
                if (subDirArray == null || subDirArray.Length <= 0)
                {
                    return new List<PluginMetadata>(0);
                }
                List<PluginMetadata> rstList = new List<PluginMetadata>(subDirArray.Length);
                foreach (var d in subDirArray)
                {
                    string metafile = Path.Combine(d.FullName, METADATA_FILE_NAME);
                    if (File.Exists(metafile) == false)
                    {
                        continue;
                    }
                    PluginMetadata data = null;
                    try
                    {
                        string c = File.ReadAllText(metafile);
                        data = JsonConvert.DeserializeObject<PluginMetadata>(c);
                    }
                    catch (Exception ex) { Logger.Error(ex); }
                    if (data != null && data.UniqueName != null && string.Compare(d.Name, GetValidPath(data.UniqueName), true) == 0)
                    {
                        rstList.Add(data);
                    }
                }
                return rstList;
            });
        }

        public static string ActionUrl(this PluginMetadata metadata, string controller, string action, object id = null)
        {
            if(metadata==null||metadata.ToString().Length==0)
            {
                throw new ArgumentNullException("metadata");
            }
            string pluginUniqueName = metadata.UniqueName;
            return ActionUrl(pluginUniqueName, controller, action, id);
        }

        public static string ActionUrl(string pluginUniqueName, string controller, string action, object id = null)
        {
            if (id == null)
            {
                return string.Format("/plugin/{0}/{1}/{2}/", pluginUniqueName, controller, action).ToLower();
            }
            return string.Format("/plugin/{0}/{1}/{2}/{3}/", pluginUniqueName, controller, action, id).ToLower();
        }

        public static string StaticFileUrl(this PluginMetadata metadata, string filePath)
        {
            if (metadata == null || metadata.ToString().Length == 0)
            {
                throw new ArgumentNullException("metadata");
            }
            string pluginUniqueName = metadata.UniqueName;
            return StaticFileUrl(pluginUniqueName, filePath);
        }

        public static string StaticFileUrl(string pluginUniqueName, string filePath)
        {
            if (string.IsNullOrWhiteSpace(filePath))
            {
                return string.Format("/plugins/{0}/", pluginUniqueName).ToLower();
            }
            filePath = filePath.Trim().Trim('~', '/', '\\').Replace('\\', '/');
            return string.Format("/plugins/{0}/{1}", GetValidPath(pluginUniqueName), filePath).ToLower();
        }

        public static PluginMetadata Current
        {
            get
            {
                if (HttpContext.Current == null)
                {
                    throw new InvalidOperationException("The property 'Current' just can be called in asp.net mvc enviroment.");
                }
                HttpContextBase contextWrapper = new HttpContextWrapper(HttpContext.Current);
                RouteData routeData = RouteTable.Routes.GetRouteData(contextWrapper);
                if (routeData == null)
                {
                    throw new InvalidOperationException("The property 'Current' just can be called in asp.net mvc enviroment.");
                }
                string pluginName = routeData.Values["plugin"] as string;
                return Plugin.GetPluginMetadata(pluginName);
            }
        }

        #region Private Or Internal

        internal static string GetPluginFolder(string pluginUniqueName)
        {
            if (string.IsNullOrWhiteSpace(pluginUniqueName))
            {
                return null;
            }
            return Path.Combine(PlugBaseFolderFullPath, Plugin.GetValidPath(pluginUniqueName));
        }

        internal static string GetPluginStatusFilePath(string pluginUniqueName)
        {
            return Path.Combine(GetPluginFolder(pluginUniqueName), STATUS_FILE_NAME);
        }

        internal static string GetPluginMetadataFilePath(string pluginUniqueName)
        {
            return Path.Combine(GetPluginFolder(pluginUniqueName), METADATA_FILE_NAME);
        }

        internal static PluginMetadata GetPluginMetadata(string pluginUniqueName)
        {
            if (string.IsNullOrWhiteSpace(pluginUniqueName))
            {
                return null;
            }
            pluginUniqueName = pluginUniqueName.Trim();
            string p = GetPluginMetadataFilePath(pluginUniqueName);

            return CacheHelper.GetWithLocalCache<PluginMetadata>("Plugin.GetPluginMetadata|" + pluginUniqueName.ToLower(), () =>
            {
                if (File.Exists(p))
                {
                    try
                    {
                        string c = File.ReadAllText(p);
                        var data = JsonConvert.DeserializeObject<PluginMetadata>(c);
                        if (data != null && data.UniqueName != null && data.UniqueName.Trim().ToLower() == pluginUniqueName.ToLower())
                        {
                            return data;
                        }
                    }
                    catch (Exception ex) { Logger.Error(ex); }
                }
                return null;
            }, p);
        }

        internal static bool SetPluginStatus(string pluginUniqueName, PluginStatus status)
        {
            PluginMetadata data = GetPluginMetadata(pluginUniqueName);
            if (data == null)
            {
                throw new ApplicationException(string.Format("The plugin '{0}' is invalid.", pluginUniqueName));
            }
            var oldStatus = data.Status;
            if (oldStatus == status)
            {
                return false;
            }
            string errorMsg = "Can't change the status from '{0}' to '{1}'.";
            string statusFile = GetPluginStatusFilePath(pluginUniqueName);
            if (status == PluginStatus.JustDeployed)
            {
                if (oldStatus != PluginStatus.ToUninstall)
                {
                    throw new ApplicationException(string.Format(errorMsg, oldStatus, status));
                }
                if (File.Exists(statusFile))
                {
                    File.Delete(statusFile);
                }
            }
            else if (status == PluginStatus.ToInstall)
            {
                if (oldStatus != PluginStatus.JustDeployed)
                {
                    throw new ApplicationException(string.Format(errorMsg, oldStatus, status));
                }
                File.WriteAllText(statusFile, "0");
            }
            else if (status == PluginStatus.Installed)
            {
                if (oldStatus != PluginStatus.ToInstall)
                {
                    throw new ApplicationException(string.Format(errorMsg, oldStatus, status));
                }
                File.WriteAllText(statusFile, "1");
            }
            else if (status == PluginStatus.ToUninstall)
            {
                if (oldStatus != PluginStatus.Installed)
                {
                    throw new ApplicationException(string.Format(errorMsg, oldStatus, status));
                }
                File.WriteAllText(statusFile, "2");
            }
            return true;
        }

        internal static string GetValidPath(string path)
        {
            char[] chars = Path.GetInvalidPathChars();
            foreach (var c in chars)
            {
                if (path.IndexOf(c) >= 0)
                {
                    path = path.Replace(c, '_');
                }
            }
            return path;
        }

        internal static void Initialize(Application application)
        {
            List<PluginMetadata> pluginList = GetAllValidPluginMetadatas();
            var uninstallList = pluginList.FindAll(x => x.Status == PluginStatus.ToUninstall);
            var installList = pluginList.FindAll(x => x.Status == PluginStatus.ToInstall);
            var hasInstalledList = pluginList.FindAll(x => x.Status == PluginStatus.Installed);
            foreach (var pluginMetadata in uninstallList)
            {
                try
                {
                    string target_dll = Path.Combine(pluginMetadata.OutputAbsolutePath, pluginMetadata.EntryAssemblyFileName);
                    if (File.Exists(target_dll) == false)
                    {
                        string projectFile = Path.Combine(Plugin.GetPluginFolder(pluginMetadata.UniqueName), pluginMetadata.ProjectFileName);
                        var rst = ProjectCompiler.Compile(projectFile, DotNetFrameworkVersion.V4_0);
                        if (rst.Success == false)
                        {
                            Logger.Error(rst.ErrorMessage);
                            continue;
                        }
                    }
                    IPlugin plugin = CreatePluginInstance(pluginMetadata);
                    if (plugin != null)
                    {
                        plugin.Uninstall(pluginMetadata);
                    }
                    SetPluginStatus(pluginMetadata.UniqueName, PluginStatus.JustDeployed);
                }
                catch (Exception ex)
                {
                    Logger.Error(ex);
                }
            }
            SortedDictionary<PluginMetadata, IPlugin> toInitializeDic = new SortedDictionary<PluginMetadata, IPlugin>();
            foreach (var pluginMetadata in installList)
            {
                try
                {
                    string target_dll = Path.Combine(pluginMetadata.OutputAbsolutePath, pluginMetadata.EntryAssemblyFileName);
                    if (File.Exists(target_dll) == false)
                    {
                        string projectFile = Path.Combine(Plugin.GetPluginFolder(pluginMetadata.UniqueName), pluginMetadata.ProjectFileName);
                        var rst = ProjectCompiler.Compile(projectFile, DotNetFrameworkVersion.V4_0);
                        if (rst.Success == false)
                        {
                            Logger.Error(rst.ErrorMessage);
                            continue;
                        }
                    }
                    IPlugin plugin = CreatePluginInstance(pluginMetadata);
                    if (plugin != null)
                    {
                        plugin.Install(pluginMetadata);
                        toInitializeDic.Add(pluginMetadata, plugin);
                    }
                    SetPluginStatus(pluginMetadata.UniqueName, PluginStatus.Installed);
                }
                catch (Exception ex)
                {
                    Logger.Error(ex);
                }
            }
            foreach (var pluginMetadata in hasInstalledList)
            {
                try
                {
                    string target_dll = Path.Combine(pluginMetadata.OutputAbsolutePath, pluginMetadata.EntryAssemblyFileName);
                    if (File.Exists(target_dll) == false)
                    {
                        string projectFile = Path.Combine(Plugin.GetPluginFolder(pluginMetadata.UniqueName), pluginMetadata.ProjectFileName);
                        var rst = ProjectCompiler.Compile(projectFile, DotNetFrameworkVersion.V4_0);
                        if (rst.Success == false)
                        {
                            Logger.Error(rst.ErrorMessage);
                            continue;
                        }
                    }
                    IPlugin plugin = CreatePluginInstance(pluginMetadata);
                    if (plugin != null)
                    {
                        toInitializeDic.Add(pluginMetadata, plugin);
                    }
                }
                catch (Exception ex)
                {
                    Logger.Error(ex);
                }
            }
            foreach (var entry in toInitializeDic)
            {
                if (entry.Value != null)
                {
                    try
                    {
                        entry.Value.Initialize(application, entry.Key);
                    }
                    catch (Exception ex)
                    {
                        Logger.Error(ex);
                    }
                }
            }
        }

        internal static void RegisterRoute(RouteCollection routes)
        {
            routes.Add(new PluginActionUrlRoute());
            //routes.Add(new PluginStaticFileUrlRoute());
        }

        internal static void ClearShadowCopiedFile()
        {
            if (Directory.Exists(SHADOW_COPY_FOLDER) == false)
            {
                Directory.CreateDirectory(SHADOW_COPY_FOLDER);
                return;
            }
            DirectoryInfo d = new DirectoryInfo(SHADOW_COPY_FOLDER);
            var binFiles = d.GetFiles("*", SearchOption.AllDirectories);
            //clear out shadow copied plugins
            foreach (var f in binFiles)
            {
                try
                {
                    File.Delete(f.FullName);
                }
                catch (Exception ex) { Logger.Error(ex); }
            }
        }

        private static IPlugin CreatePluginInstance(PluginMetadata metadata)
        {
            if (metadata == null)
            {
                return null;
            }
            string root = metadata.OutputAbsolutePath;
            if (string.IsNullOrWhiteSpace(root) || Directory.Exists(root) == false)
            {
                return null;
            }
            DirectoryInfo d = new DirectoryInfo(root);
            var pluginFiles = d.GetFiles("*.dll", SearchOption.TopDirectoryOnly).ToList();
            var mainPluginFile = pluginFiles.Find(x => x.Name.Equals(metadata.EntryAssemblyFileName, StringComparison.InvariantCultureIgnoreCase));
            if (mainPluginFile == null)
            {
                return null;
            }
            Assembly mainAssembly = PerformFileDeploy(mainPluginFile);
            foreach (var plugin in pluginFiles
                    .FindAll(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase) && !IsAlreadyLoaded(x)))
            {
                PerformFileDeploy(plugin);
            }
            // init plugin type (only one plugin per assembly is allowed)
            var exportedTypes = mainAssembly.GetExportedTypes().ToList();
            Type type = exportedTypes.Find(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface && t.IsClass && !t.IsAbstract);
            if (type != null)
            {
                return Activator.CreateInstance(type) as IPlugin;
            }
            return null;
        }

        internal static Assembly PerformFileDeploy(FileInfo plug)
        {
            if (plug.Directory == null || plug.Directory.Parent == null)
            {
                throw new InvalidOperationException("The plugin directory for the " + plug.Name +
                                                    " file exists in a folder outside of the allowed folder hierarchy");
            }
            FileInfo shadowCopiedPlug;

            if (WebHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
            {
                //all plugins will need to be copied to ~/Plugins/bin/
                //this is aboslutely required because all of this relies on probingPaths being set statically in the web.config

                //were running in med trust, so copy to custom bin folder
                var shadowCopyPlugFolder = new DirectoryInfo(SHADOW_COPY_FOLDER);
                if (shadowCopyPlugFolder.Exists == false)
                {
                    shadowCopyPlugFolder.Create();
                }
                shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
            }
            else
            {
                var directory = AppDomain.CurrentDomain.DynamicDirectory;
                //were running in full trust so copy to standard dynamic folder
                shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
            }

            //we can now register the plugin definition
            var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName));

            //add the reference to the build manager
            try
            {
                BuildManager.AddReferencedAssembly(shadowCopiedAssembly);
            }
            catch (Exception ex)
            {
                Logger.Error(ex);
            }

            return shadowCopiedAssembly;
        }

        private static FileInfo InitializeMediumTrust(FileInfo plugin, DirectoryInfo shadowCopyPlugFolder)
        {
            var shouldCopy = true;
            var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plugin.Name));

            //check if a shadow copied file already exists and if it does, check if it's updated, if not don't copy
            if (shadowCopiedPlug.Exists)
            {
                //it's better to use LastWriteTimeUTC, but not all file systems have this property
                //maybe it is better to compare file hash?
                var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plugin.CreationTimeUtc.Ticks;
                if (areFilesIdentical)
                {
                    shouldCopy = false;
                }
                else
                {
                    File.Delete(shadowCopiedPlug.FullName);
                }
            }

            if (shouldCopy)
            {
                try
                {
                    File.Copy(plugin.FullName, shadowCopiedPlug.FullName, true);
                }
                catch (IOException)
                {
                    //this occurs when the files are locked,
                    //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
                    //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
                    try
                    {
                        var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
                        File.Move(shadowCopiedPlug.FullName, oldFile);
                    }
                    catch (IOException exc)
                    {
                        throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
                    }
                    //ok, we've made it this far, now retry the shadow copy
                    File.Copy(plugin.FullName, shadowCopiedPlug.FullName, true);
                }
            }

            return shadowCopiedPlug;
        }

        private static FileInfo InitializeFullTrust(FileInfo plugin, DirectoryInfo shadowCopyPlugFolder)
        {
            var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plugin.Name));
            try
            {
                File.Copy(plugin.FullName, shadowCopiedPlug.FullName, true);
            }
            catch (IOException)
            {
                //this occurs when the files are locked,
                //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
                //which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
                try
                {
                    var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
                    File.Move(shadowCopiedPlug.FullName, oldFile);
                }
                catch (IOException exc)
                {
                    throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
                }
                //ok, we've made it this far, now retry the shadow copy
                File.Copy(plugin.FullName, shadowCopiedPlug.FullName, true);
            }
            return shadowCopiedPlug;
        }

        private static Assembly FindLoadedAssemblyFromDomain(FileInfo fileInfo)
        {
            //do not compare the full assembly name, just filename
            try
            {
                string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
                foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
                {
                    string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault();
                    if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase))
                    {
                        return a;
                    }
                }
            }
            catch (Exception ex) { Logger.Error(ex); }
            return null;
        }

        internal static bool IsAlreadyLoaded(FileInfo fileInfo)
        {
            return FindLoadedAssemblyFromDomain(fileInfo) != null;
        }

        #endregion
    }
}
